# gozerbot/bot.py
# 
#
#

""" this module contains the Bot class, which implements the bot """

__copyright__ = 'this file is in the public domain'

from gozerbot.generic import rlog, handle_exception, getlistensocket, \
stripident, checkchan, waitforqueue
from gozerbot.commands import cmnds
from gozerbot.callbacks import callbacks
from gozerbot.plugins import plugins
from gozerbot.users import users
from gozerbot.datadir import datadir
from gozerbot.partyline import partyline
from gozerbot.monitor import outmonitor
from gozerbot.irc import Irc
from gozerbot.ircevent import Ircevent
from gozerbot.limlist import Limlist
from gozerbot.channels import Channels
from gozerbot.pdod import Pdod
from gozerbot.config import config
from gozerbot.wait import Privwait
from gozerbot.eventhandler import Commandhandler
import gozerbot.thr as thr
import re, socket, struct, Queue, time, types

dccchatre = re.compile('\001DCC CHAT CHAT (\S+) (\d+)\001', re.I)

class Bot(Irc):

    """ class that dispatches commands and checks for callbacks to fire """ 

    def __init__(self, owner, name = 'main'):
        self.owner = owner.lower()
        # see if owner already has a user account if not merge otherwise add
        try:
            username = users.getname(self.owner)
            if not username:
                merged = users.merge('owner', self.owner)
                if not merged:
                    users.add('owner', [self.owner], perms = ['USER', 'OPER'])
        except Exception, ex:
            handle_exception()
        Irc.__init__(self, name) # call parent constructor
        self.backlog = Limlist(1000) # backlog of ircevents
        # init channels
        self.channels = Channels(datadir + '/%s.channels' % self.name) 
        self.joinedchannels = []
        self.state = Pdod(datadir + '/%s.state' % self.name) # bots state
        self.userhosts = {} # dict for userhosts cache
        # object used to wait for PRIVMSG
        self.privwait = Privwait()
        # channels where we are op
        self.opchan = []
        self.commandhandler = Commandhandler()
        self.plugins = plugins
        self.splitted = []

    def __str__(self):
        return "Bot ==> name=%s channels:%s " % (self.name, self.joinedchannels)

    def connect(self, nick, server, port, passwd=None, ipv6=None):
        """ call irc connect and join channels """
        if Irc.connect(self, nick, server, port, passwd, ipv6):
            for i in self.channels.getchannelswithkeys():
                if not i:
                    continue
                if i[0] in ['&', '#', '+', '!']:
                    self.join(i)
            return 1

    def send(self, txt):
        """ call Irc senf and check for monitor callbacks """
        Irc.send(self, txt)
        outmonitor.callcb(self.name, txt)

    def save(self):
        """ saves channels and state """
        self.channels.save()
        self.state.save()

    def stop(self):
        """ stop the bot """
        self.stopreadloop = 1
        self.stopped = 1
        self.eventhandler.stop = 1
        self.eventhandler.go.put_nowait('bla')
        self.commandhandler.stop = 1
        self.commandhandler.go.put_nowait('bla')
        rlog(10, self.name, 'stopped')

    def exit(self):
        """ save data, quit the bot and do shutdown """
        if self.connected.isSet():
            self.quit(config['quitmsg'])
        self.stop()
        partyline.stop(self)
        self.shutdown()
        self.save()
        rlog(10, self.name, 'exit')

    def join(self, channel, password=None):
        """ join a channel .. use optional password """
        if not self.channels.has_key(channel):
            # init channel data
            self.channels.setdefault(channel, {})
        chan = self.channels[channel]
        # if password is provided set it
        if password:
            chan['key'] = password
        # check for control char .. if its not there init to !
        if not chan.has_key('cc'):
            chan['cc'] = '!'
        # do the actual join
        Irc.join(self, channel, password)

    def say(self, printto, what, who=None, how='msg', fromm=None):
        """ output what to printto """
        # check if printto is a queue if so put output to the queue
        if type(printto) == type(Queue.Queue): 
            printto.put_nowait('[%s] %s' % (self.name, what))
            return
        # call irc.say
        Irc.say(self, printto, what, who, how, fromm)

    def handle_privmsg(self, ievent):
        """ check if PRIVMSG is command, if so dispatch """
        if not ievent.txt:
            return
        # check if it is a dcc chat request
        chat = re.search(dccchatre, ievent.txt)
        if chat:
            # check if the user is known
            if users.allowed(ievent.userhost, 'USER'):
                # start connection
                thr.start_new_thread(self.dccconnect, \
(ievent.nick, ievent.userhost, chat.group(1), chat.group(2))) 
                return
        # see if base class method would handle it
        if Irc.handle_privmsg(self, ievent):
            return
        # set bot and socket in ircevent
        ievent.bot = self
        ievent.sock = self.sock
        # check for /msg
        if ievent.channel == self.nick.lower():
            ievent.msg = 1
            ievent.printto = ievent.nick
            if ievent.txt[0] in ['!', '@']:
                ievent.txt = ievent.txt[1:]
            plugins.trydispatch(self, ievent)
            return
        ievent.printto = ievent.channel
        # see if we can get channel control character
        try:
            cchar = self.channels[ievent.channel]['cc']
        except LookupError:
            cchar = '!'
        except TypeError:
            cchar = '!'
        # see if cchar matches, if so dispatch
        if ievent.txt[0] == cchar:
            ievent.txt = ievent.txt[1:]
            plugins.trydispatch(self, ievent)
            return
        # see if were adressed, if so dispatch
        txtlist = ievent.txt.split(':', 1)
        if txtlist[0].lower() == self.nick.lower():
            if len(txtlist) < 2:
                return
            ievent.txt = txtlist[1].strip()
            plugins.trydispatch(self, ievent)
            return
        # habbie addressing mode
        txtlist = ievent.txt.split(',', 1)
        if txtlist[0].lower() == self.nick.lower():
            if len(txtlist) < 2:
                return
            ievent.txt = txtlist[1].strip()
            plugins.trydispatch(self, ievent)
            return
        # check for PRIVMSG waiting callback
        self.privwait.check(ievent)

    def handle_join(self, ievent):
        """ handle joins """
        chan = ievent.channel
        nick = ievent.nick.lower()
        # see if its the bot who is joining
        if nick == self.nick.lower():
            if not chan in self.joinedchannels:
                # check if we already have a channels object, if not init it
                if not self.channels.has_key(chan):
                    self.channels[chan] = {}
                # send who so we can collect userhosts of joined users
                self.joinedchannels.append(chan)
            self.who(chan)
            return
        # sync joined user with userhosts cache
        self.userhosts[nick] = ievent.userhost

    def handle_kick(self, ievent):
        """ handle kick event """
        nick = ievent.nick.lower()
        chan = ievent.channel.lower()
        # see if its the bot who got kicked .. if so remove from
        # joinedchannels
        if nick == self.nick.lower():
            if chan in self.joinedchannels:
                self.joinedchannels.remove(chan)

    def handle_nick(self, ievent):
        """ update userhost cache on nick change """
        self.userhosts[ievent.postfix.lower()] = ievent.userhost

    def handle_part(self, ievent):
        """ handle parts """
        chan = ievent.channel
        # see if its the bot who is parting
        if ievent.nick.lower() == self.nick.lower():
            rlog(1, self.name, 'parted channel %s' % chan)
            # remove from joinedchannels
            if chan in self.joinedchannels:
                self.joinedchannels.remove(chan)

    def handle_ievent(self, ievent):
        """ check for callbacks, call Irc class method .. add to backlog """
        j = Ircevent()
        j.copyin(ievent)
        callbacks.check(self, j)
        Irc.handle_ievent(self, ievent)
        self.backlog.append(ievent)

    def handle_311(self, ievent):
        """ handle 311 response .. sync with userhosts cache """
        post = ievent.postfix.split()
        userhost = "%s@%s" % (post[2], post[3])
        userhost = stripident(userhost.lower())
        rlog(1, self.name, 'adding %s to userhosts: %s' % (post[1].lower(), \
userhost))
        self.userhosts[post[1].lower()] = userhost

    def handle_352(self, ievent):
        """ handle 352 response .. sync with userhosts cache """
        post = ievent.postfix.split()
        userhost = "%s@%s" % (post[2], post[3])
        userhost = stripident(userhost.lower())
        rlog(1, self.name, 'adding %s to userhosts: %s' % (post[5].lower(), \
userhost))
        self.userhosts[post[5].lower()] = userhost

    def handle_353(self, ievent):
        """ handle 352 response .. sync with userhosts cache """
        userlist = ievent.txt.split()
        for i in userlist:
            if i[0] == '@' and i[1:].lower() == self.nick.lower():
                if ievent.channel not in self.opchan:
                    self.opchan.append(ievent.channel)

    def handle_invite(self, ievent):
        """ join channel if invited by OPER """
        if users.allowed(ievent.userhost, ['OPER', ]):
            self.join(ievent.txt)

    def dccconnect(self, nick, userhost, addr, port):
        """ connect to dcc request from nick """
        try:
            port = int(port)
            if re.search(':', addr):
                rlog(1, self.name, 'creating ipv6 socket for dcc chat with %s'\
% nick)
                sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
                sock.connect((addr, port))
            else:
                rlog(1, self.name, 'creating ipv4 socket for dcc chat with %s'\
 % nick)
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                sock.connect((addr, port))
        except Exception, ex:
            rlog(10 , self.name, 'dcc error: %s' % str(ex))
            return
        # were connected .. start dcc loop
        self.dodcc(sock, nick, userhost)

    def dcclisten(self, nick, userhost, channel):
        """ accept dcc chat requests """
        try:
            listenip = socket.gethostbyname(socket.gethostname())
            (port, listensock) = getlistensocket(listenip)
            ipip2 = socket.inet_aton(listenip)
            ipip = struct.unpack('>L', ipip2)[0]
            chatmsg = 'DCC CHAT CHAT %s %s' % (ipip, port)
            self.ctcp(nick, chatmsg)
            sock = listensock.accept()[0]
        except Exception, ex:
            rlog(10 , self.name, 'dcc error: %s' % str(ex))
            return
        # connected
        self.dodcc(sock, nick, userhost, channel)

    def dodcc(self, sock, nick, userhost, channel=None):
        """ loop for dcc commands """
        try:
            # send welcome message .. show list of commands for USER perms
            cmndslist = cmnds.list('USER')
            cmndslist.sort()
            sock.send('Welcome to PYJBOT ' + nick + " ;]\n")
            sock.send('commands we support: ' + ' '.join(cmndslist) + '\n')
            sock.send('add chan <channel> if you want to use a channel\n')
            sock.send("control character is ! .. bot broadcast is @\n")
            sockfile = sock.makefile('r')
        except Exception, ex:
            rlog(10 , self.name, 'dcc error: %s' % str(ex))
            return
        # add joined user to the partyline
        partyline.add_party(self, sock, nick)
        # big loop
        while 1:
            time.sleep(0.001)
            try:
                # read from socket
                res = sockfile.readline()
                # if res == "" than the otherside had disconnected
                if self.stopped or not res:
                    rlog(1, self.name, 'closing dcc with ' + nick)
                    partyline.del_party(nick)
                    return
            except Exception, ex:
                rlog(10, self.name, 'closing dcc with ' + nick)
                partyline.del_party(nick)
                return
            try:
                # see if user provided channel
                res = res.strip()
                chan = checkchan(res)
                if chan != None:
                    (channel, res) = chan
                else:
                    channel = ''
                # create ircevent
                ievent = Ircevent()
                ievent.nick = nick
                ievent.userhost = userhost
                ievent.channel = channel
                ievent.origtxt = res
                ievent.txt = res
                ievent.cmnd = 'DCC'
                ievent.bot = self
                ievent.sock = sock
                ievent.speed = 10
                # check if its a command if so dispatch
                if ievent.txt[0] == "!":
                    ievent.txt = ievent.txt[1:]
                    if plugins.trydispatch(self, ievent):
                        continue
                elif ievent.txt[0] == "@":
                    partyline.say_broadcast_notself(ievent.nick, \
"[%s] %s" % (ievent.nick, ievent.txt))
                    q = Queue.Queue()
                    ievent.queues = [q]
                    ievent.txt = ievent.txt[1:]
                    plugins.trydispatch(self, ievent)
                    result = waitforqueue(q)
                    if result:
                        for i in result:
                            partyline.say_broadcast("[bot] %s" % i)
                    else:
                        partyline.say_broadcast("[bot] no result")
                    continue
                else:
                    partyline.say_broadcast_notself(ievent.nick, \
"[%s] %s" % (ievent.nick, ievent.txt))
                # check PRIVMSG wait
                self.privwait.check(ievent)
            except Exception, ex:
                if type(ex) == types.ListType:
                    (errno, errstr) = ex
                else:
                    errno = 0
                    errstr = str(ex)
                if errno == 32:
                    break
                handle_exception()
        rlog(1, self.name, 'closing dcc with ' + nick)
